import {getBindingKey, triggerKeyEvent} from "./keyboard.js";
import * as event from "./../system/event.js";


// Analog deadzone
let deadzone = 0.1;

/**
 * Normalize axis values for wired Xbox 360
 * @ignore
 */
function wiredXbox360NormalizeFn(value, axis, button) {
    if (button === this.GAMEPAD.BUTTONS.L2 || button === this.GAMEPAD.BUTTONS.R2) {
        return (value + 1) / 2;
    }
    return value;
}

/**
 * Normalize axis values for OUYA
 * @ignore
 */
function ouyaNormalizeFn(value, axis, button) {
    if (value > 0) {
        if (button === this.GAMEPAD.BUTTONS.L2) {
            // L2 is wonky; seems like the deadzone is around 20000
            // (That's over 15% of the total range!)
            value = Math.max(0, value - 20000) / 111070;
        }
        else {
            // Normalize [1..65536] => [0.0..0.5]
            value = (value - 1) / 131070;
        }
    }
    else {
        // Normalize [-65536..-1] => [0.5..1.0]
        value = (65536 + value) / 131070 + 0.5;
    }

    return value;
}

// Match vendor and product codes for Firefox
const vendorProductRE = /^([0-9a-f]{1,4})-([0-9a-f]{1,4})-/i;

// Match leading zeros
const leadingZeroRE = /^0+/;

/**
 * Firefox reports different ids for gamepads depending on the platform:
 * - Windows: vendor and product codes contain leading zeroes
 * - Mac: vendor and product codes are sparse (no leading zeroes)
 *
 * This function normalizes the id to support both formats
 * @ignore
 */
function addMapping(id, mapping) {
    const expanded_id = id.replace(vendorProductRE, (_, a, b) =>
        "000".slice(a.length - 1) + a + "-" +
        "000".slice(b.length - 1) + b + "-"
    );
    const sparse_id = id.replace(vendorProductRE, (_, a, b) =>
        a.replace(leadingZeroRE, "") + "-" +
        b.replace(leadingZeroRE, "") + "-"
    );

    // Normalize optional parameters
    mapping.analog = mapping.analog || mapping.buttons.map(() => -1);
    mapping.normalize_fn = mapping.normalize_fn || function (value) { return value; };

    remap.set(expanded_id, mapping);
    remap.set(sparse_id, mapping);
}

// binding list
let bindings = {};

// mapping list
let remap = new Map();

let updateEventHandler;

// Default gamepad mappings
[
    // Firefox mappings
    [
        "45e-28e-Xbox 360 Wired Controller",
        {
            "axes" : [ 0, 1, 3, 4 ],
            "buttons" : [ 11, 12, 13, 14, 8, 9, -1, -1, 5, 4, 6, 7, 0, 1, 2, 3, 10 ],
            "analog" : [ -1, -1, -1, -1, -1, -1, 2, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
            "normalize_fn" : wiredXbox360NormalizeFn
        }
    ],
    [
        "54c-268-PLAYSTATION(R)3 Controller",
        {
            "axes" : [ 0, 1, 2, 3 ],
            "buttons" : [ 14, 13, 15, 12, 10, 11, 8, 9, 0, 3, 1, 2, 4, 6, 7, 5, 16 ]
        }
    ],
    [
        "54c-5c4-Wireless Controller", // PS4 Controller
        {
            "axes" : [ 0, 1, 2, 3 ],
            "buttons" : [ 1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 17, 12, 13 ]
        }
    ],
    [
        "2836-1-OUYA Game Controller",
        {
            "axes" : [ 0, 3, 7, 9 ],
            "buttons" : [ 3, 6, 4, 5, 7, 8, 15, 16, -1, -1, 9, 10, 11, 12, 13, 14, -1 ],
            "analog" : [ -1, -1, -1, -1, -1, -1, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
            "normalize_fn" : ouyaNormalizeFn
        }
    ],

    // Chrome mappings
    [
        "OUYA Game Controller (Vendor: 2836 Product: 0001)",
        {
            "axes" : [ 0, 1, 3, 4 ],
            "buttons" : [ 0, 3, 1, 2, 4, 5, 12, 13, -1, -1, 6, 7, 8, 9, 10, 11, -1 ],
            "analog" : [ -1, -1, -1, -1, -1, -1, 2, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
            "normalize_fn" : ouyaNormalizeFn
        }
    ]
].forEach((value) => {
    addMapping(value[0], value[1]);
});

/**
 * Update gamepad status
 * @ignore
 */
let updateGamepads = function () {
    let gamepads = navigator.getGamepads();

    // Trigger button bindings
    Object.keys(bindings).forEach((index) => {
        let gamepad = gamepads[index];
        if (!gamepad) {
            return;
        }

        let mapping = null;
        if (gamepad.mapping !== "standard") {
            mapping = remap.get(gamepad.id);
        }

        let binding = bindings[index];

        // Iterate all buttons that have active bindings
        Object.keys(binding.buttons).forEach((button) => {
            let last = binding.buttons[button];
            let mapped_button = button;
            let mapped_axis = -1;

            // Remap buttons if necessary
            if (mapping) {
                mapped_button = mapping.buttons[button];
                mapped_axis = mapping.analog[button];
                if (mapped_button < 0 && mapped_axis < 0) {
                    // Button is not mapped
                    return;
                }
            }

            // Get mapped button
            let current = gamepad.buttons[mapped_button] || {};

            // Remap an axis to an analog button
            if (mapping) {
                if (mapped_axis >= 0) {
                    let value = mapping.normalize_fn(gamepad.axes[mapped_axis], -1, +button);

                    // Create a new object, because GamepadButton is read-only
                    current = {
                        "value" : value,
                        "pressed" : current.pressed || (Math.abs(value) >= deadzone)
                    };
                }
            }

            event.emit(event.GAMEPAD_UPDATE, index, "buttons", +button, current);

            // Edge detection
            if (!last.pressed && current.pressed) {
                triggerKeyEvent(last.keyCode, true, mapped_button + 256);
            }
            else if (last.pressed && !current.pressed) {
                triggerKeyEvent(last.keyCode, false, mapped_button + 256);
            }

            // Update last button state
            last.value = current.value;
            last.pressed = current.pressed;
        });

        // Iterate all axes that have active bindings
        Object.keys(binding.axes).forEach((axis) => {
            let last = binding.axes[axis];
            let mapped_axis = axis;

            // Remap buttons if necessary
            if (mapping) {
                mapped_axis = mapping.axes[axis];
                if (mapped_axis < 0) {
                    // axe is not mapped
                    return;
                }
            }

            // retrieve the current value and normalize if necessary
            let value = gamepad.axes[mapped_axis];
            if (typeof(value) === "undefined") {
                return;
            }
            if (mapping) {
                value = mapping.normalize_fn(value, +axis, -1);
            }
            // normalize value into a [-1, 1] range value (treat 0 as positive)
            let range = Math.sign(value) || 1;
            if (last[range].keyCode === 0) {
                return;
            }
            let pressed = (Math.abs(value) >= (deadzone + Math.abs(last[range].threshold)));

            event.emit(event.GAMEPAD_UPDATE, index, "axes", +axis, value);

            // Edge detection
            if (!last[range].pressed && pressed) {
                // Release the opposite direction, if necessary
                if (last[-range].pressed) {
                    triggerKeyEvent(last[-range].keyCode, false, mapped_axis + 256);
                    last[-range].value = 0;
                    last[-range].pressed = false;
                }

                triggerKeyEvent(last[range].keyCode, true, mapped_axis + 256);
            }
            else if ((last[range].pressed || last[-range].pressed) && !pressed) {
                range = last[range].pressed ? range : -range;
                triggerKeyEvent(last[range].keyCode, false, mapped_axis + 256);
            }

            // Update last axis state
            last[range].value = value;
            last[range].pressed = pressed;
        });
    });
};

// gamepad connected callback
if (globalThis.navigator && typeof globalThis.navigator.getGamepads === "function") {
    globalThis.addEventListener("gamepadconnected", (e) => {
        event.emit(event.GAMEPAD_CONNECTED, e.gamepad);
    }, false);

    /*
     * gamepad disconnected callback
     */
    globalThis.addEventListener("gamepaddisconnected", (e) => {
        event.emit(event.GAMEPAD_DISCONNECTED, e.gamepad);
    }, false);
}

/*
 * PUBLIC STUFF
 */

/**
 * Namespace for standard gamepad mapping constants
 * @public
 * @namespace GAMEPAD
 * @memberof input
 */
export let GAMEPAD = {
    /**
     * Standard gamepad mapping information for axes<br>
     * <ul>
     *   <li>Left control stick: <code>LX</code> (horizontal), <code>LY</code> (vertical)</li>
     *   <li>Right control stick: <code>RX</code> (horizontal), <code>RY</code> (vertical)</li>
     *   <li>Extras: <code>EXTRA_1</code>, <code>EXTRA_2</code>, <code>EXTRA_3</code>, <code>EXTRA_4</code></li>
     * </ul>
     * @public
     * @name AXES
     * @enum {number}
     * @memberof input.GAMEPAD
     * @see https://w3c.github.io/gamepad/#remapping
     */
    "AXES" : {
        "LX"        : 0,
        "LY"        : 1,
        "RX"        : 2,
        "RY"        : 3,
        "EXTRA_1"   : 4,
        "EXTRA_2"   : 5,
        "EXTRA_3"   : 6,
        "EXTRA_4"   : 7
    },

    /**
     * Standard gamepad mapping information for buttons<br>
     * <ul>
     *   <li>Face buttons: <code>FACE_1</code>, <code>FACE_2</code>, <code>FACE_3</code>, <code>FACE_4</code></li>
     *   <li>D-Pad: <code>UP</code>, <code>DOWN</code>, <code>LEFT</code>, <code>RIGHT</code></li>
     *   <li>Shoulder buttons: <code>L1</code>, <code>L2</code>, <code>R1</code>, <code>R2</code></li>
     *   <li>Analog stick (clicks): <code>L3</code>, <code>R3</code></li>
     *   <li>Navigation: <code>SELECT</code> (<code>BACK</code>), <code>START</code> (<code>FORWARD</code>), <code>HOME</code></li>
     *   <li>Extras: <code>EXTRA_1</code>, <code>EXTRA_2</code>, <code>EXTRA_3</code>, <code>EXTRA_4</code></li>
     * </ul>
     * @public
     * @name BUTTONS
     * @enum {number}
     * @memberof input.GAMEPAD
     * @see https://w3c.github.io/gamepad/#remapping
     */
    "BUTTONS" : {
        "FACE_1"    : 0,
        "FACE_2"    : 1,
        "FACE_3"    : 2,
        "FACE_4"    : 3,
        "L1"        : 4,
        "R1"        : 5,
        "L2"        : 6,
        "R2"        : 7,
        "SELECT"    : 8,
        "BACK"      : 8,
        "START"     : 9,
        "FORWARD"   : 9,
        "L3"        : 10,
        "R3"        : 11,
        "UP"        : 12,
        "DOWN"      : 13,
        "LEFT"      : 14,
        "RIGHT"     : 15,
        "HOME"      : 16,
        "EXTRA_1"   : 17,
        "EXTRA_2"   : 18,
        "EXTRA_3"   : 19,
        "EXTRA_4"   : 20
    }
};

/**
 * Associate a gamepad event to a keycode
 * @name bindGamepad
 * @memberof input
 * @public
 * @param {number} index - Gamepad index
 * @param {object} button - Button/Axis definition
 * @param {string} button.type - "buttons" or "axes"
 * @param {number} button.code - button or axis code id (See {@link input.GAMEPAD.BUTTONS}, {@link input.GAMEPAD.AXES})
 * @param {number} [button.threshold=1] - value indicating when the axis should trigger the keycode (e.g. -0.5 or 0.5)
 * @param {number} keyCode - (See {@link input.KEY})
 * @example
 * // enable the keyboard
 * me.input.bindKey(me.input.KEY.X, "shoot");
 * ...
 * // map the lower face button on the first gamepad to the X key
 * me.input.bindGamepad(0, {type:"buttons", code: me.input.GAMEPAD.BUTTONS.FACE_1}, me.input.KEY.X);
 * // map the left axis value on the first gamepad to the LEFT key
 * me.input.bindGamepad(0, {type:"axes", code: me.input.GAMEPAD.AXES.LX, threshold: -0.5}, me.input.KEY.LEFT);
 */
export function bindGamepad(index, button, keyCode) {
    // Throw an exception if no action is defined for the specified keycode
    if (!getBindingKey(keyCode)) {
        throw new Error("no action defined for keycode " + keyCode);
    }

    // register to the the update event if not yet done and supported by the browser
    // if not supported, the function will fail silently (-> update loop won't be called)
    if (typeof updateEventHandler === "undefined" && typeof navigator.getGamepads === "function") {
        updateEventHandler = event.on(event.GAME_BEFORE_UPDATE, updateGamepads);
    }

    // Allocate bindings if not defined
    if (!bindings[index]) {
        bindings[index] = {
            "axes" : {},
            "buttons" : {}
        };
    }

    let mapping = {
        "keyCode" : keyCode,
        "value" : 0,
        "pressed" : false,
        "threshold" : button.threshold // can be undefined
    };
    let binding = bindings[index][button.type];

    // Map the gamepad button or axis to the keycode
    if (button.type === "buttons") {
        // buttons are defined by a `gamePadButton` object
        binding[button.code] = mapping;
    } else if (button.type === "axes") {
        // normalize threshold into a value that can represent both side of the axis
        let range = (Math.sign(button.threshold) || 1);
        // axes are defined using two objects; one for negative and one for positive
        if (!binding[button.code]) {
            binding[button.code] = {};
        }
        let axes = binding[button.code];
        axes[range] = mapping;

        // Ensure the opposite axis exists
        if (!axes[-range]) {
            axes[-range] = {
                "keyCode" : 0,
                "value" : 0,
                "pressed" : false,
                "threshold" : -range
            };
        }
    }
}

/**
 * unbind the defined keycode
 * @name unbindGamepad
 * @memberof input
 * @public
 * @param {number} index - Gamepad index
 * @param {number} button - (See {@link input.GAMEPAD.BUTTONS})
 * @example
 * me.input.unbindGamepad(0, me.input.GAMEPAD.BUTTONS.FACE_1);
 */
export function unbindGamepad(index, button) {
    if (!bindings[index]) {
        throw new Error("no bindings for gamepad " + index);
    }
    bindings[index].buttons[button] = {};
}

/**
 * Set deadzone for analog gamepad inputs<br>
 * The default deadzone is 0.1 (10%) Analog values less than this will be ignored
 * @name setGamepadDeadzone
 * @memberof input
 * @public
 * @param {number} value - Deadzone value
 */
export function setGamepadDeadzone(value) {
    deadzone = value;
}

/**
 * specify a custom mapping for a specific gamepad id<br>
 * see below for the default mapping : <br>
 * <center><img src="images/gamepad_diagram.png"/></center><br>
 * @name setGamepadMapping
 * @memberof input
 * @public
 * @param {string} id - Gamepad id string
 * @param {object} mapping - A hash table
 * @param {number[]} mapping.axes - Standard analog control stick axis locations
 * @param {number[]} mapping.buttons - Standard digital button locations
 * @param {number[]} [mapping.analog] - Analog axis locations for buttons
 * @param {Function} [mapping.normalize_fn] - a function that returns a normalized value in range [-1.0..1.0] for the given value, axis and button
 * @example
 * // A weird controller that has its axis mappings reversed
 * me.input.setGamepadMapping("Generic USB Controller", {
 *   "axes" : [ 3, 2, 1, 0 ],
 *   "buttons" : [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]
 * });
 *
 * // Mapping extra axes to analog buttons
 * me.input.setGamepadMapping("Generic Analog Controller", {
 *   "axes" : [ 0, 1, 2, 3 ],
 *   "buttons" : [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ],
 *
 *   // Raw axis 4 is mapped to GAMEPAD.BUTTONS.FACE_1
 *   // Raw axis 5 is mapped to GAMEPAD.BUTTONS.FACE_2
 *   // etc...
 *   // Also maps left and right triggers
 *   "analog" : [ 4, 5, 6, 7, -1, -1, 8, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1 ],
 *
 *   // Normalize the value of button L2: [-1.0..1.0] => [0.0..1.0]
 *   "normalize_fn" : function (value, axis, button) {
 *     return ((button === me.input.GAMEPAD.BUTTONS.L2) ? ((value + 1) / 2) : value) || 0;
 *   }
 * });
 */
export let setGamepadMapping = addMapping;
